package com.szeldon.pj.jps.data.store;

import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.logging.Logger;

import nu.xom.Attribute;
import nu.xom.Builder;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Elements;
import nu.xom.ParsingException;
import nu.xom.ValidityException;

import com.szeldon.pj.jps.datastore.BooleanObject;
import com.szeldon.pj.jps.datastore.ComplexObject;
import com.szeldon.pj.jps.datastore.IntegerObject;
import com.szeldon.pj.jps.datastore.RealObject;
import com.szeldon.pj.jps.datastore.ReferenceObject;
import com.szeldon.pj.jps.datastore.SBAObject;
import com.szeldon.pj.jps.datastore.SBAObjectId;
import com.szeldon.pj.jps.datastore.StringObject;

public class HashMapSBAStore extends SBAStore {

	private static final BigInteger ADDED_FOR_NEXT_ID = new BigInteger("1");

	private static final String TYPE_ATTRIBUTE_NAME = "type";
	private static final String STRING_TYPE_ATTRIBUTE_VALUE = "string";
	private static final String INTEGER_TYPE_ATTRIBUTE_VALUE = "integer";
	private static final String REAL_TYPE_ATTRIBUTE_VALUE = "real";
	private static final String COMPLEX_TYPE_ATTRIBUTE_VALUE = "complex";
	private static final String BOOLEAN_TYPE_ATTRIBUTE_VALUE = "boolean";

	private static Logger logger = Logger.getLogger(HashMapSBAStore.class.getName());

	private HashMap<SBAObjectId, SBAObject> sbaObjectsHashMap = new HashMap<SBAObjectId, SBAObject>();

	private BigInteger currentSBAObjectId = new BigInteger("0");

	private SBAObjectId entrySBAObjectId;

	private boolean isInitialized = false;

	@Override
	public void abstractInitializeStore() {

		logger.info("initialising store");

		entrySBAObjectId = synchronizedGenerateUniqueSBAObjectId();
		sbaObjectsHashMap.put(entrySBAObjectId, new ComplexObject(entrySBAObjectId, SBAStore.ROOT_ENTRY_NAME));
		isInitialized = true;

		logger.info("store initialised (entry with id=[" + entrySBAObjectId.toString() + "])");
	}

	//
	// CRUD start
	//

	// create
	@Override
	public StringObject createStringInComplex(String name, String value, ComplexObject complexObject) {
		checkIfStoreIsInitialized();

		SBAObjectId id = synchronizedGenerateUniqueSBAObjectId();

		StringObject newString = new StringObject(id, name, value);
		sbaObjectsHashMap.put(id, newString);
		complexObject.addSbaObjectId(id);

		logger.finer("Created StringObject with id=" + id + " and added it to ComplexObject with id="
				+ complexObject.getSbaObjectId());

		return newString;
	}

	@Override
	public IntegerObject createIntegerInComplex(String name, int value, ComplexObject complexObject) {
		checkIfStoreIsInitialized();

		SBAObjectId id = synchronizedGenerateUniqueSBAObjectId();

		IntegerObject newInteger = new IntegerObject(id, name, value);
		sbaObjectsHashMap.put(id, newInteger);
		complexObject.addSbaObjectId(id);

		logger.finer("Created IntegerObject with id=" + id + " and added it to ComplexObject with id="
				+ complexObject.getSbaObjectId());

		return newInteger;
	}

	@Override
	public RealObject createRealInComplex(String name, double value, ComplexObject complexObject) {
		checkIfStoreIsInitialized();

		SBAObjectId id = synchronizedGenerateUniqueSBAObjectId();

		RealObject newReal = new RealObject(id, name, value);
		sbaObjectsHashMap.put(id, newReal);
		complexObject.addSbaObjectId(id);

		logger.finer("Created RealObject with id=" + id + " and added it to ComplexObject with id="
				+ complexObject.getSbaObjectId());

		return newReal;
	}

	@Override
	public BooleanObject createBooleanInComplex(String name, boolean value, ComplexObject complexObject) {
		checkIfStoreIsInitialized();

		SBAObjectId id = synchronizedGenerateUniqueSBAObjectId();

		BooleanObject newReal = new BooleanObject(id, name, value);
		sbaObjectsHashMap.put(id, newReal);
		complexObject.addSbaObjectId(id);

		logger.finer("Created BooleanObject with id=" + id + " and added it to ComplexObject with id="
				+ complexObject.getSbaObjectId());

		return newReal;
	}

	@Override
	public ComplexObject createComplexInComplex(String name, ComplexObject complexObject) {
		checkIfStoreIsInitialized();

		SBAObjectId id = synchronizedGenerateUniqueSBAObjectId();

		ComplexObject newComplex = new ComplexObject(id, name);
		sbaObjectsHashMap.put(id, newComplex);
		complexObject.addSbaObjectId(id);

		logger.finer("Created ComplexObject with id=" + id + " and added it to ComplexObject with id="
				+ complexObject.getSbaObjectId());

		return newComplex;
	}

	@Override
	public ReferenceObject createReferenceInComplex(String name, SBAObjectId value, ComplexObject complexObject) {
		// TODO Auto-generated method stub
		throw new UnsupportedOperationException("Not YET implemented.");
	}

	@Override
	protected SBAObjectId abstractGenerateUniqueSBAObjectId() {
		currentSBAObjectId = currentSBAObjectId.add(ADDED_FOR_NEXT_ID);
		return new SBAObjectId(currentSBAObjectId);
	}

	// retrieve
	@Override
	public SBAObject retrieve(SBAObjectId sbaObjectId) {
		return sbaObjectsHashMap.get(sbaObjectId);
	}

	@Override
	public ComplexObject retrieveEntryObject() {
		return (ComplexObject) sbaObjectsHashMap.get(entrySBAObjectId);
	}

	// update
	@Override
	public boolean updateBooleanValue(SBAObjectId updatedObjectId, boolean newValue) {
		SBAObject objectToBeUpdated = sbaObjectsHashMap.get(updatedObjectId);

		if (objectToBeUpdated == null) {
			return false;
		}
		if (objectToBeUpdated instanceof BooleanObject) {
			((BooleanObject) objectToBeUpdated).setValue(newValue);
			return true;
		} else {
			throw new IllegalArgumentException("Can't update value of " + objectToBeUpdated.getClass().getSimpleName()
					+ " object with boolean.");
		}
	}

	@Override
	public boolean updateComplexObjectByAdding(SBAObjectId updatedObjectId, SBAObjectId addedSubobjectId) {
		SBAObject objectToBeUpdated = sbaObjectsHashMap.get(updatedObjectId);

		if (objectToBeUpdated == null) {
			return false;
		}
		if (objectToBeUpdated instanceof ComplexObject) {
			((ComplexObject) objectToBeUpdated).addSbaObjectId(addedSubobjectId);
			return true;
		} else {
			throw new IllegalArgumentException("Can't update value of " + objectToBeUpdated.getClass().getSimpleName()
					+ " object with SBAObjectId.");
		}
	}

	@Override
	public boolean updateComplexObjectByRemoval(SBAObjectId updatedObjectId, SBAObjectId removedSubobjectId) {
		SBAObject objectToBeUpdated = sbaObjectsHashMap.get(updatedObjectId);

		if (objectToBeUpdated == null) {
			return false;
		}
		if (objectToBeUpdated instanceof ComplexObject) {
			SBAObjectId removedId = ((ComplexObject) objectToBeUpdated).removeSbaObjectId(removedSubobjectId);
			if (removedId == null) {
				return false;
			}
			return true;
		} else {
			throw new IllegalArgumentException("Can't update value of " + objectToBeUpdated.getClass().getSimpleName()
					+ " object with SBAObjectId.");
		}
	}

	@Override
	public boolean updateIntegerValue(SBAObjectId updatedObjectId, int newValue) {
		SBAObject objectToBeUpdated = sbaObjectsHashMap.get(updatedObjectId);

		if (objectToBeUpdated == null) {
			return false;
		}
		if (objectToBeUpdated instanceof IntegerObject) {
			((IntegerObject) objectToBeUpdated).setValue(newValue);
			return true;
		} else {
			throw new IllegalArgumentException("Can't update value of " + objectToBeUpdated.getClass().getSimpleName()
					+ " object with int.");
		}
	}

	@Override
	public boolean updateRealValue(SBAObjectId updatedObjectId, double newValue) {
		SBAObject objectToBeUpdated = sbaObjectsHashMap.get(updatedObjectId);

		if (objectToBeUpdated == null) {
			return false;
		}
		if (objectToBeUpdated instanceof RealObject) {
			((RealObject) objectToBeUpdated).setValue(newValue);
			return true;
		} else {
			throw new IllegalArgumentException("Can't update value of " + objectToBeUpdated.getClass().getSimpleName()
					+ " object with double.");
		}
	}

	@Override
	public boolean updateStringValue(SBAObjectId updatedObjectId, String newValue) {
		SBAObject objectToBeUpdated = sbaObjectsHashMap.get(updatedObjectId);

		if (objectToBeUpdated == null) {
			return false;
		}
		if (objectToBeUpdated instanceof StringObject) {
			((StringObject) objectToBeUpdated).setValue(newValue);
			return true;
		} else {
			throw new IllegalArgumentException("Can't update value of " + objectToBeUpdated.getClass().getSimpleName()
					+ " object with String.");
		}
	}

	// delete
	@Override
	public SBAObject delete(SBAObjectId sbaObjectId) {
		SBAObject deletedObject = sbaObjectsHashMap.remove(sbaObjectId);

		if (deletedObject == null) {
			return null;
		}

		// deleting subobjects if this complex object
		if (deletedObject instanceof ComplexObject) {
			ComplexObject deletedComplexObject = (ComplexObject) deletedObject;
			for (SBAObjectId currentSubobjectId : deletedComplexObject.getSubobjectsList()) {
				SBAObject currentSubobject = sbaObjectsHashMap.get(currentSubobjectId);
				delete(currentSubobjectId);
			}
		}

		// deleting of this object in its parent TODO: stupid! checks ALL objects for not containing deleted object!
		for (SBAObjectId currentSbaObjectId : sbaObjectsHashMap.keySet()) {
			SBAObject currentSbaObject = sbaObjectsHashMap.get(currentSbaObjectId);
			if (currentSbaObject instanceof ComplexObject) {
				ComplexObject currentComplexObject = (ComplexObject) currentSbaObject;
				if (currentComplexObject.getSubobjectsList().contains(sbaObjectId)) {
					currentComplexObject.getSubobjectsList().remove(sbaObjectId);
				}
			}
		}

		return deletedObject;
	}

	//
	// CRUD end
	//
	@Override
	public boolean loadObjectsFromXML(String filePath) throws ValidityException, ParsingException, IOException,
			IllegalStateException {
		checkIfStoreIsInitialized();

		logger.info("Loading objects from file: " + filePath);

		boolean objectsLoaded = false;

		Builder parser = new Builder();
		Document document = parser.build(new File(filePath));
		Element root = document.getRootElement();

		objectsLoaded = loadObjectsFromElementIntoObject(root,
				(ComplexObject) (sbaObjectsHashMap.get(entrySBAObjectId)));

		if (objectsLoaded) {
			logger.info("Objects loaded from file.");
			logger.info(sbaObjectsHashMap.toString());
		} else {
			logger.info("Objects not loaded from file.");
		}

		return objectsLoaded;
	}

	@Override
	public String currentStateToStandardNotation() {
		StringBuffer buffer = new StringBuffer();
		for (SBAObjectId currentSbaObjectId : sbaObjectsHashMap.keySet()) {
			SBAObject currentSbaObject = sbaObjectsHashMap.get(currentSbaObjectId);
			buffer.append(currentSbaObject.toString() + "\n");
		}
		return buffer.toString();
	}

	private boolean loadObjectsFromElementIntoObject(Element parentElement, ComplexObject complexParentSbaObject) {

		boolean objectsLoaded = false;
		Elements children = parentElement.getChildElements();
		for (int i = 0; i < children.size(); i++) {
			Element currentChild = children.get(i);

			Attribute typeAttribute = currentChild.getAttribute(TYPE_ATTRIBUTE_NAME);
			String type = typeAttribute.getValue();

			String name = currentChild.getLocalName();
			String value = currentChild.getValue();

			if (type.equals(STRING_TYPE_ATTRIBUTE_VALUE)) {
				createStringInComplex(name, value, complexParentSbaObject);
			} else if (type.equals(INTEGER_TYPE_ATTRIBUTE_VALUE)) {
				createIntegerInComplex(name, Integer.parseInt(value), complexParentSbaObject);
			} else if (type.equals(REAL_TYPE_ATTRIBUTE_VALUE)) {
				createRealInComplex(name, Double.parseDouble(value), complexParentSbaObject);
			} else if (type.equals(BOOLEAN_TYPE_ATTRIBUTE_VALUE)) {
				createBooleanInComplex(name, Boolean.parseBoolean(value), complexParentSbaObject);
			} else if (type.equals(COMPLEX_TYPE_ATTRIBUTE_VALUE)) {
				ComplexObject complexSubobject = createComplexInComplex(name, complexParentSbaObject);
				objectsLoaded = loadObjectsFromElementIntoObject(currentChild, complexSubobject);

				if (!objectsLoaded) {
					return false;
				}
			} else {
				throw new IllegalArgumentException("'" + type + "' isn't supported as type of SBA object.");
			}
		}
		objectsLoaded = true;
		return objectsLoaded;

	}

	/**
	 * Checks if store is initialized. Throws {@link IllegalStateException} if store isn't initialized.
	 */
	private void checkIfStoreIsInitialized() {
		if (!isInitialized) {
			throw new IllegalStateException("Can't use store before it was initialized.");
		}
	}

}
